/*
 * // Bristleback plugin - Copyright (c) 2010 bristleback.googlecode.com
 * // ---------------------------------------------------------------------------
 * // This program is free software; you can redistribute it and/or modify it
 * // under the terms of the GNU Lesser General Public License as published by the
 * // Free Software Foundation; either version 3 of the License, or (at your
 * // option) any later version.
 * // This library is distributed in the hope that it will be useful,
 * // but without any warranty; without even the implied warranty of merchantability
 * // or fitness for a particular purpose.
 * // You should have received a copy of the GNU Lesser General Public License along
 * // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
 * // ---------------------------------------------------------------------------
 */

package pl.bristleback.server.bristle.utils;

import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Utility class providing convenient methods for retrieving {@link org.json.JSONObject} objects from beans and collections.
 * <p/>
 * Created on: 2010-11-21 16:19:03 <br/>
 *
 * @author Wojciech Niemiec
 */
public final class JsonUtil {
  private static Logger log = Logger.getLogger(JsonUtil.class.getName());

  /**
   * Unsupported operation.
   */
  private JsonUtil() {
    throw new UnsupportedOperationException();
  }

  /**
   * Creates Json object from bean. Every field with getter method in proper Bean convention will be processed.
   * Optionally, deep search of fields can be performed.
   * It means that if collection of objects or another bean will be found inside processed bean,
   * they will be also processed into Json objects and placed in parent Json object.
   * Those operations will be performed recursively, so it may cause infinite loop if not used correctly.
   * See {@link pl.bristleback.server.bristle.utils.JsonUtil#getJsonArray(java.util.Collection, boolean)}
   * to know how arrays and collection are processed into JSONArray.
   *
   * @param bean       bean to process.
   * @param deepSearch true if nested collections or beans should be processed into Json objects.
   * @return {@link org.json.JSONObject} representing bean given as parameter.
   * @throws org.json.JSONException thrown when Json cannot be created for some reason.
   */
  public static JSONObject getJsonFromBean(Object bean, boolean deepSearch) throws JSONException {
    return processComplexObject(bean, deepSearch);
  }

  /**
   * Creates Json object from bean, using getters specified by user. Optionally, deep search of fields can be performed.
   * It means that if collection of objects or another bean will be found inside processed bean,
   * they will be also processed into Json objects and placed in parent Json object.
   * Those operations will be performed recursively, so it may cause infinite loop if not used correctly.
   * See {@link pl.bristleback.server.bristle.utils.JsonUtil#getJsonArray(java.util.Collection, boolean)}
   * to know how arrays and collection are processed into JSONArray.
   *
   * @param bean       bean to process.
   * @param deepSearch true if nested collections or beans should be processed into Json objects.
   * @param getters    list of properties that should be processed. Note that if deepSearch parameter is set to false,
   *                   beans and collections will be not processed even if they are on getter list.
   * @return {@link org.json.JSONObject} representing bean given as parameter.
   * @throws org.json.JSONException thrown when Json cannot be created for some reason.
   */
  public static JSONObject getJsonFromBean(Object bean, boolean deepSearch, List<Getter> getters) throws JSONException {
    return processComplexObject(bean, deepSearch, getters);
  }

  public static Object processObject(Object value, boolean deepSearch) throws JSONException {
    if (value instanceof Collection) {
      return getJsonArray((Collection) value, deepSearch);
    } else if (value.getClass().isArray()) {
      if (Object[].class.isAssignableFrom(value.getClass())) {
        return getJsonArray((Object[]) value, deepSearch);
      } else {
        return getJsonPrimitiveArray(value);
      }
    } else if (value instanceof Map) {
      return getJsonObjectFromMap((Map) value, deepSearch);
    } else if ((value instanceof JSONArray) || (value instanceof JSONObject)) {
      return value;
    } else if (ReflectionUtil.isRawType(value)) {
      return value;
    } else {
      return processComplexObject(value, deepSearch);
    }
  }

  private static JSONObject processComplexObject(Object bean, boolean deepSearch) throws JSONException {
    List<Getter> getters = ReflectionUtil.getGetterMethods(bean.getClass(), true);
    return processComplexObject(bean, deepSearch, getters);
  }

  private static JSONObject processComplexObject(Object bean, boolean deepSearch, List<Getter> getters) throws JSONException {
    JSONObject json = new JSONObject();
    for (Getter getter : getters) {
      Object value = getter.invoke(bean);
      if (value == null) {
        continue;
      }
      if (ReflectionUtil.isRawType(value) || deepSearch) {
        json.put(getter.getFieldName(), processObject(value, deepSearch));
      }
    }
    return json;
  }

  /**
   * Creates Json objects from map given as parameter.
   * Map entries will be processed into corresponding Json objects with its key and value structure.
   * Optionally, deep search of fields can be performed.
   * It means that if collection of objects or another bean will be found inside processed entry value,
   * they will be also processed into Json objects and placed in parent Json object.
   * Those operations will be performed recursively, so it may cause infinite loop if not used correctly.
   *
   * @param map        map to process.
   * @param deepSearch true if nested collections or beans should be processed into Json objects.
   * @return {@link org.json.JSONObject} with field name given as map keys and field values given as map values.
   * @throws org.json.JSONException thrown when Json element cannot be created for some reason.
   */
  public static JSONObject getJsonObjectFromMap(Map<?, ?> map, boolean deepSearch) throws JSONException {
    JSONObject json = new JSONObject();
    if (map.isEmpty()) {
      return json;
    }
    Set<? extends Map.Entry<?, ?>> entries = map.entrySet();
    for (Map.Entry entry : entries) {
      String key = entry.getKey().toString();
      Object value = processObject(entry.getValue(), deepSearch);
      json.put(key, value);
    }
    return json;
  }

  /**
   * Creates Json array from collection given as parameter.
   * It is very important to know that in case when elements in this collection are beans,
   * this method takes getters only from first element of collection,
   * assuming that every element is an object of the same class.
   * Optionally, deep search of fields can be performed.
   * It means that if collection of objects or another bean will be found inside processed collection value,
   * they will be also processed into Json objects and placed in parent Json object.
   * Those operations will be performed recursively, so it may cause infinite loop if not used correctly.
   *
   * @param collection collection of elements that should be elements of Json array.
   * @param deepSearch true if nested collections or beans should be processed into Json objects.
   * @return {@link org.json.JSONArray} with elements represented by elements given in collection.
   * @throws org.json.JSONException thrown when Json element cannot be created for some reason.
   */
  public static JSONArray getJsonArray(Collection collection, boolean deepSearch) throws JSONException {
    return getJsonArray(collection.toArray(), deepSearch);
  }

  /**
   * Creates Json array from array given as parameter.
   * It is very important to know that in case when elements in this array are beans,
   * this method takes getters only from first index of array,
   * assuming that every element is an object of the same class.
   * Optionally, deep search of fields can be performed.
   * It means that if collection of objects or another bean will be found inside processed array value,
   * they will be also processed into Json objects and placed in parent Json object.
   * Those operations will be performed recursively, so it may cause infinite loop if not used correctly.
   *
   * @param beanArray  array that should be processed into elements of Json array.
   * @param deepSearch true if nested collections or beans should be processed into Json objects.
   * @return {@link org.json.JSONArray} with elements represented by elements given in array.
   * @throws org.json.JSONException thrown when Json element cannot be created for some reason.
   */
  public static JSONArray getJsonArray(Object[] beanArray, boolean deepSearch) throws JSONException {
    JSONArray array = new JSONArray();

    if (beanArray.length == 0) {
      return array;
    }
    if (!deepSearch || ReflectionUtil.isRawType(beanArray[0])) {
      getJsonRawArray(beanArray, array);
    } else {
      getJsonDeepSearchArray(beanArray, array);
    }
    return array;
  }

  private static void getJsonDeepSearchArray(Object[] collection, JSONArray array) throws JSONException {
    Object firstElement = collection[0];
    boolean complexObject;

    if (firstElement instanceof Collection) {
      complexObject = false;
    } else if (firstElement instanceof Object[]) {
      complexObject = false;
    } else if (firstElement instanceof Map) {
      complexObject = false;
    } else if ((firstElement instanceof JSONArray) || (firstElement instanceof JSONObject)) {
      complexObject = false;
    } else {
      complexObject = true;
    }
    if (complexObject) {
      List<Getter> getters = ReflectionUtil.getGetterMethods(firstElement.getClass(), true);
      for (Object element : collection) {
        array.put(processComplexObject(element, true, getters));
      }
    }
  }

  private static JSONArray getJsonPrimitiveArray(Object arrayObject) {
    JSONArray jsonArray = new JSONArray();
    int length = Array.getLength(arrayObject);
    for (int i = 0; i < length; i++) {
      jsonArray.put(Array.get(arrayObject, i));
    }
    return jsonArray;
  }

  private static void getJsonRawArray(Object[] beanArray, JSONArray array) {
    for (Object bean : beanArray) {
      array.put(bean);
    }
  }
}
